Skip to content

fix(auth): offer the API-key login on server-mode admin 403s - #1569

Open
paoloantinori wants to merge 3 commits into
debpalash:mainfrom
paoloantinori:fix/remote-admin-403-auth-gate
Open

fix(auth): offer the API-key login on server-mode admin 403s#1569
paoloantinori wants to merge 3 commits into
debpalash:mainfrom
paoloantinori:fix/remote-admin-403-auth-gate

Conversation

@paoloantinori

@paoloantinori paoloantinori commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Fixes #1568

What

  • Frontend (client.ts): a 403 whose detail names the admin API key now routes into the existing ov:auth-required flow (mode: 'apikey'), exactly like the 401 paths — RemoteAuthGate shows the API-key form, the app tree unmounts, and the infinite-retry polling stops instead of spamming the console. Other 403s (CSRF "browser origin rejected", desktop-only routes) deliberately stay plain errors, because presenting a key cannot satisfy them.
  • Backend (dependencies.py): new _admin_gate_403() emits a mode-distinct detail. Server mode keeps "loopback origin or admin API key required" (a key genuinely satisfies the gate there); desktop mode now answers "loopback origin required" — truthful, since the admin-credential checks only run under _server_mode(), so no presented key can ever satisfy the desktop gate. Without this split, routing on the message alone would trap a desktop PIN-share guest in a login form that can never succeed.
  • Lockstep test (tests/test_auth_gate_detail_lockstep.py): pins the literal contract across layers — the frontend matcher ("admin api key") must keep matching the backend's server-mode detail and must NOT match the details a key cannot fix. A backend reword without updating client.ts fails CI instead of silently regressing to 403 spam.

Tests

  • New frontend cases in client.test.ts: admin-gate 403 dispatches {mode:"apikey"}; CSRF 403 does not dispatch (fail-before/pass-after).
  • New backend cases in test_loopback_server_mode.py: per-mode detail for require_admin + require_admin_action (desktop → plain loopback, server+key → key-naming).
  • Updated the two desktop-arm assertions in test_engines_route_shape.py to the new truthful detail.
  • Suites run on top of current main: backend 208 passed, frontend api 95 passed; oxfmt --check clean.

Verification

On a live LAN deployment (server mode + OMNIVOICE_API_KEY): before, a remote browser logged /sysinfo, /model/status, /system/notifications 403s every few seconds indefinitely. After: 3 initial 403s, the API-key gate renders, the exchange produces a short-lived session, and all polls return 200.

An adversarial review pass caught the first version of the frontend-only fix breaking desktop LAN-share guests (unsatisfiable key form); the mode-distinct backend detail is the root-cause fix for that class.

Server-mode admin 403 responses now trigger API-key authentication, while unrelated 403 responses remain errors and stale responses cannot clear newer sessions. Backend details now reflect mode and API-key availability, with cross-layer tests covering routing and recovery. Review the pre-existing watermark route coverage failure separately; mismatched error details could still break authentication routing.

Review follow-ups (commit e2bfeaa)

Greptile's review on 014e18c flagged two P1s, both fixed with fail-before/pass-after tests:

  1. Unsatisfiable PIN-only login gate (dependencies.py): server mode + share PIN + no OMNIVOICE_API_KEY closed read-only bootstrap while no key existed to present, so the key-naming detail gated the whole UI behind a form whose exchange can never succeed. _admin_gate_403() now names the key only when server mode and an API key is configured; PIN-only and bare servers answer the plain loopback detail (truthful: only loopback can use admin when no key exists), which the SPA leaves a plain error. New test: test_require_admin_pin_only_server_mode_detail_is_plain_loopback.
  2. Stale 403 clears new session (client.ts): a late 403/401 landing after a key exchange wiped the fresh session (clearAdminSession() was unconditional), reloading a successful login straight back into the gate. A failed response may now only invalidate the credentials it actually carried (send-time session capture + token compare). New test: a stale 403 does not clear a session stored during its flight.

CodeRabbit's docstring-coverage warning on the new test functions is also addressed.

…sh#1568)

Since debpalash#1525 the /system router sits behind require_admin, so an
unauthenticated non-loopback browser gets 403 where it used to get 401.
The SPA routed only 401s into the ov:auth-required flow, so a remote
browser spammed 403s on /sysinfo, /model/status, /system/notifications
forever (polling hooks retry infinitely) and was never offered the
master-for-session exchange.

Frontend: route a 403 whose detail names the admin API key into the
existing apikey gate; other 403s (CSRF, desktop-only) stay plain errors
because no key can satisfy them.

Backend: _admin_gate_403() emits a mode-distinct detail — server mode
keeps "loopback origin or admin API key required" (a key genuinely
satisfies it), desktop mode answers "loopback origin required"
(truthful: the credential checks only run under server mode, so
routing desktop guests to a key form would loop forever).

tests/test_auth_gate_detail_lockstep.py locks the literal contract
across layers so a backend reword fails CI unless client.ts moves with
it. Verified end-to-end on a live LAN deployment: gate renders after 3
initial 403s, key exchange produces a session, all polls 200.
@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 273a0ae2-f8f9-444e-a7d2-32b4f95818be

📥 Commits

Reviewing files that changed from the base of the PR and between e2bfeaa and adff800.

📒 Files selected for processing (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md

Included review availability: Your plan includes up to 8 reviews per rolling hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

Changes

The backend now returns mode-specific admin-gate 403 details. Server-mode API-key errors enter the frontend authentication flow. Desktop, CSRF, and unrelated 403 responses remain ordinary errors. Tests and documentation cover the behavior.

Admin 403 authentication

Layer / File(s) Summary
Mode-specific admin-gate responses
backend/api/dependencies.py, tests/test_loopback_server_mode.py
require_admin and require_admin_action use a shared response helper. Server mode with an API key reports the admin API-key requirement. Other modes report the loopback-only requirement.
API-key authentication routing
frontend/src/api/client.ts, frontend/src/api/client.test.ts
apiFetch routes matching 403 details to ov:auth-required with API-key mode. Other 403 responses do not trigger authentication. Stale failed requests do not clear newer sessions.
Cross-layer validation and documentation
tests/test_auth_gate_detail_lockstep.py, tests/backend/api/test_engines_route_shape.py, docs/api-auth.md, CHANGELOG.md
Tests enforce backend/frontend detail matching and update route expectations. Documentation and the changelog describe the distinct 403 behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to adff8

This change routes only fixable server-mode admin 403 responses into API-key authentication while preserving plain errors for cases a key cannot resolve; no actionable merge-blocking risk remains after normal checks and review.

Possibly related PRs

🚥 Pre-merge checks | ✅ 8 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (8 passed)
Check name Status Explanation
Title check ✅ Passed The title uses Conventional Commit syntax with the auth scope, describes the main change, and the issue reference appears in the body.
Description check ✅ Passed The description covers the change, implementation, tests, verification, issue context, and review follow-ups, but omits several template headings.
Linked Issues check ✅ Passed The changes address issue #1568 by enabling API-key login for eligible 403 responses, preserving unsatisfiable-error behavior, and preventing stale-session invalidation.
Out of Scope Changes check ✅ Passed The frontend, backend, documentation, and regression tests are directly related to issue #1568 and its authentication-gate requirements.
Cross-Platform Default Parity ✅ Passed The changed gate uses explicit OMNIVOICE_SERVER_MODE/API-key configuration, not OS detection; the default desktop path is shared across macOS, Windows, and Linux.
I18n Completeness (21 Locales) ✅ Passed The PR adds no frontend t() keys or rendered text. Existing RemoteAuthGate keys used by the new flow exist in all 21 locale files.
Local-First Guarantee ✅ Passed The PR diff adds only local admin-gate handling and tests; it adds no cloud URLs, dependencies, telemetry, or reporting path, and reuses the existing local /api/auth/session and OMNIVOICE_API_KEY f...
Backward Compatibility ✅ Passed The PR diff only changes admin-auth handling, docs, and tests; it adds no schema, migration, voice/project/settings, or engine/model artifact changes that require migration or re-download.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR routes server-mode admin-gate failures into API-key authentication while preserving plain errors where no key can satisfy the gate.

  • Makes backend admin-gate details depend on server mode and actual API-key availability.
  • Prevents stale failed requests from clearing a newer administrator session.
  • Adds frontend, backend, and cross-layer contract coverage for both behaviors.

Important Files Changed

Filename Overview
backend/api/dependencies.py The mode- and configuration-aware 403 detail uses the same normalized API-key source as credential validation, completing the prior PIN-only fix.
frontend/src/api/client.ts Admin-gate 403 routing and send-time token comparison preserve newly exchanged sessions while retaining normal handling for unrelated 403 responses.
frontend/src/api/client.test.ts Tests cover admin-gate routing, unrelated 403 handling, and the stale-response session race.
tests/test_loopback_server_mode.py Tests pin desktop, keyed-server, and PIN-only server detail behavior across both admin dependencies.
tests/test_auth_gate_detail_lockstep.py Cross-layer tests enforce that only key-satisfiable backend details match the frontend authentication discriminator.
docs/api-auth.md Authentication documentation reflects the mode-distinct admin-gate response contract.

Reviews (3): Last reviewed commit: "Merge remote-tracking branch 'upstream/m..." | Re-trigger Greptile

Comment thread backend/api/dependencies.py
Comment thread frontend/src/api/client.ts Outdated
…e-403 race

Greptile P1 #1 (unsatisfiable PIN-only gate): in server mode with a
share PIN but no OMNIVOICE_API_KEY, the PIN closes read-only bootstrap
while no key exists to present — the key-naming detail sent the browser
to a login form whose exchange can never succeed, hiding the whole app
behind an unrecoverable gate. _admin_gate_403() now names the key only
when server mode AND an API key is configured; PIN-only and bare
servers answer the plain loopback detail, which is truthful (only
loopback can use admin when no key exists) and keeps the SPA out of
the gate.

Greptile P1 debpalash#2 (stale 403 clears new session): a late 403/401 landing
after a key exchange wiped the fresh session via the unconditional
clearAdminSession(), reloading a successful login straight back into
the gate. A failed response may now only invalidate the credentials it
actually carried (send-time session capture + token compare before
clearing).

Also adds the docstrings CodeRabbit's coverage check asked for on the
new test functions.
@paoloantinori

Copy link
Copy Markdown
Contributor Author

The red Tests (backend + frontend) check is pre-existing on main, not introduced by this PR: tests/test_watermark_route_coverage.py::test_every_synthesis_module_routes_through_mark_synthetic fails identically on pristine upstream/main (ee35d238) — offender worker/transport/server.py synthesizes audio without the mark_synthetic chokepoint (#1169). Everything else in the matrix is green on e2bfeaa0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remote browser UI spams 403 on /sysinfo, /model/status, /system/notifications and never offers the API-key login

1 participant